package pl.cezary.butler.walili.client;

import static pl.cezary.butler.walili.server.db.utils.UnitInterruptedException.isInterrupted;
import pl.cezary.butler.walili.client.mime.IMimeTypeHandler;
import pl.cezary.butler.walili.common.IServerFacadeRemote;
import pl.cezary.butler.walili.common.TaskStatus;
import pl.cezary.butler.walili.common.Task;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.rmi.RemoteException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.springframework.context.ApplicationContext;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.tidy.Tidy;
import pl.cezary.butler.walili.client.network.INetworkStats;
import pl.cezary.butler.walili.server.db.tasks.DbTaskInterrupted;
import pl.cezary.butler.walili.server.db.utils.UnitInterruptedException;
import pl.cezary.butler.walili.server.utils.MonitoredInputStream;

public class TaskExecutionUnit implements Runnable {

    private static AtomicInteger watki = new AtomicInteger(0);
    private static final Logger log = Logger.getLogger(TaskExecutionUnit.class.getName());
    private static final Set<String> MIME_TYPES;
    private static final Map<String, String> NODE_TYPES;
    private static INetworkStats stats;

    public static INetworkStats getStats() {
        if (stats == null) {
            stats = ClientApp.initSpringContext().getBean(INetworkStats.class);
        }
        return stats;
    }
    private static final ApplicationContext context;
    private final IMimeTypeHandler mimeHandler = context.getBean(IMimeTypeHandler.class);

    static {
        context = ClientApp.initSpringContext(); //TODO potrzebny client context;
    }

    static {
        MIME_TYPES = Collections.unmodifiableSet(
                new HashSet<String>(
                Arrays.asList(new String[]{"text/html", "text/xhtml"})));
        final Map<String, String> nodeTypes = new HashMap<String, String>(2);
        nodeTypes.put("a", "href");
        nodeTypes.put("img", "src");
        NODE_TYPES = Collections.unmodifiableMap(nodeTypes);
    }
    private final Task zadanie;
    private final IServerFacadeRemote fasada;
    private final Queue<Node> kolejka = new LinkedList<Node>();
    private final Set<Task> unikalneZadania = new HashSet<Task>();
    private int foundHyperlinks = 0;

    public TaskExecutionUnit(Task zad, IServerFacadeRemote f) {
        watki.incrementAndGet();
        zadanie = zad;
        fasada = f;
        unikalneZadania.add(zad);
    }

    private void failure() throws UnitInterruptedException {
        if (isInterrupted()) {
            return;
        }
        try {
            if (log.isLoggable(Level.FINE)) {
                log.log(Level.FINE, "Strona :{0} by\u0142a niedost\u0119pna", zadanie.getCelURL());
            }
            zadanie.setStatus(TaskStatus.PORAZKA);
            fasada.taskDone(zadanie);
        } catch (RemoteException ex) {
            log.log(Level.SEVERE, "Problem komunikacyjny", ex);
        }
    }

    private void processNode(Node node) throws RemoteException, DOMException, UnitInterruptedException {
        if (isInterrupted()) {
            return;
        }
        if (node == null) {
            return;
        }
        Node local_wez = node.getNextSibling();
        if (local_wez != null) {
            kolejka.offer(local_wez);
        }
        local_wez = node.getFirstChild();
        if (local_wez != null) {
            kolejka.offer(local_wez);
        }
        final String nodeName = node.getNodeName();
        final String atrybutDlaWezla = (nodeName != null) ? NODE_TYPES.get(nodeName.toLowerCase()) : null;
        if (nodeName != null && atrybutDlaWezla != null) {
            NamedNodeMap atrybuty = node.getAttributes();
            if (atrybuty != null) {
                Node attrybut = atrybuty.getNamedItem(atrybutDlaWezla);
                if (attrybut != null) {
                    final String target = attrybut.getNodeValue();
                    if (target != null) {
                        final Task task = zadanie.tworzZadanie(target);
                        if (!task.ignorable()) {
                            foundHyperlinks++;
                            if (!unikalneZadania.contains(task)) {
                                fasada.sendTask(task);
                                unikalneZadania.add(task);
                            }
                        }
                    }
                }
            }
        }
    }

    private void success() throws UnitInterruptedException {
        if (isInterrupted()) {
            return;
        }
        try {
            log.log(Level.FINE, "Strona: {0} została pozytywnie zweryfikowania", zadanie.getCelURL());
            zadanie.setStatus(TaskStatus.SUKCES);
            fasada.taskDone(zadanie);
        } catch (RemoteException ex) {
            log.log(Level.SEVERE, "Problem komunikacyjny", ex);
        }
    }

    private void executeTask() {
        InputStream raw = null;
        try {
            try {
                URL url = zadanie.getCelURL();
                URLConnection connection = url.openConnection();
                assert(connection!=null);
                if (url.getProtocol().equals("http")) {
                    if (((HttpURLConnection) connection).getResponseCode() != 200) {
                        failure();
                    } else {
                        String typ;
                        typ = ((HttpURLConnection) connection).getContentType(); //TODO verify network traffic                        
                        typ = (typ!=null)?typ.split(";")[0]:"text/html";
                        zadanie.setMimeType(typ);
                        if (!MIME_TYPES.contains(typ.toLowerCase())) {
                            final boolean handled = mimeHandler.handleMimeType(typ, connection);
                            if (connection.getContent() != null) {
                                if (!handled) {
                                    getStats().read(connection.getContentLength()); //TODO verify network traffic
                                }
                                success();
                            } else {
                                failure();
                            }
                        } else {
                            Tidy tidy = new Tidy();
                            //tidy.setXHTML(true);
                            tidy.setQuiet(true);
                            tidy.setErrout(new PrintWriter(new DummyOutputStream()));
                            raw = new MonitoredInputStream(url.openStream(),getStats());
                            Document pars = tidy.parseDOM(raw, null);

                            kolejka.offer(pars.getFirstChild());

                            while (!(kolejka.isEmpty() || isInterrupted())) {
                                processNode(kolejka.poll());
                            }
                            log.fine(String.format("Na stronie %s znalazlem %d odnosnikow w tym %d unikalnych", zadanie.getCel(), foundHyperlinks, unikalneZadania.size()));
                            getStats().read(connection.getContentLength());
                            success();
                        }
                    }
                } else {
                    raw = new MonitoredInputStream(url.openStream(),getStats());
                    if (raw.read() == -1) {
                        failure();
                    } else {
                        success();
                    }
                }
            } catch (DbTaskInterrupted ie) {
                log.log(Level.INFO, "Task unit interrupted", ie);
            } catch (RemoteException r) {
                log.log(Level.SEVERE, "Błąd zdalnego wywołania", r);
            } catch (DOMException d) {
                log.log(Level.INFO, "Błąd schematu DOM - nieprawidłowa składnia strony");
            } catch (IOException e) {
                log.log(Level.INFO, "Problem z we wy, prawdopodobnie strona niedostepna", e);
                failure();
            } catch (UnitInterruptedException uie) {
                throw uie;
            } catch (Exception e) {
                log.log(Level.SEVERE, String.format("Unexpected error while trying to analyze %s",zadanie.getCel()), e);
                failure();
            } finally {
                if (raw != null) {
                    raw.close();
                }
            }
        } catch (UnitInterruptedException uie) {
            log.log(Level.INFO, "Task execution unit for {0} was interrupted",zadanie.getCelURL());
        } catch (IOException e) {
            log.log(Level.SEVERE, "Błąd we/wy", e);
        }
    }

    @Override
    public void run() {
        executeTask();
        watki.decrementAndGet();
    }

    public static int running() {
        return watki.intValue();
    }

}
